#ifndef __M_MATCHER_H__
#define __M_MATCHER_H__

#include <list>
#include <mutex>
#include <condition_variable>
#include "util.hpp"
#include "db.hpp"
#include "room.hpp"
#include "online.hpp"

// 下面实现匹配队列
// 匹配队列本质就是一个阻塞队列，当队列中有>2个元素时，则pop()前两个元素，然后为他们开辟房间
// 我们将来会为每一个阻塞队列创建一个匹配的线程，这个线程其实就是消费者，用于判断当前队列中用户的个数是否超过2个，如果超过2个，则出队头的前两个用户，为这两个用户创建游戏房间

// 所以我们需要先实现队列的基本属性
// 每一个队列其实是一个阻塞队列，将来有可能大量的青铜用户在青铜队列里等着被匹配成功，但不排除有人还没匹配成功就退出了
// 这时我们就得将他移出队列，但是queue做不到，所以只能使用双向链表list实现
// 这个阻塞队列本质是一个生产者消费者模型，有两个线程，一个往里塞用户，一个往外取用户，所以锁和条件变量就得派上用场了，条件变量是用来解决锁竞争的问题的

template <class T>
class match_queue
{
public:
    match_queue()
    {
    }
    ~match_queue()
    {
    }

    // 入数据，查看队列元素是否>2，若>2，则唤醒一次消费线程
    void push(const T &data)
    {
        // 我们的项目是每一个阻塞队列匹配一个消费者线程，所以唤醒时notify_all和notify_one都是可以的
        std::unique_lock<std::mutex> lock(_mtx);
        _list.push_back(data);

        // 在push这里，我们实现的思想是，只要入队一次数据那我们就唤醒一次线程，让线程去看看是否现在能出队用户进行游戏房间的创建了
        _cond.notify_all();
    }

    // 出队头数据 + 获取数据(data是输出型参数)
    bool pop(T &data)
    {
        // 我们这里的pop只是pop一次队头的元素，而不是pop两次
        // 因为你无法保证队列里面此刻就有两个用户，我们以输出型参数的方式来将用户的uid进行返回，将其交给外部
        std::unique_lock<std::mutex> lock(_mtx);
        if (_list.empty())
            return false;

        data = _list.front();
        _list.pop_front();
        return true;
    }

    // 移除指定数据
    void remove(T &data)
    {
        std::unique_lock<std::mutex> lock(_mtx);
        _list.remove(data);
    }

    // 获取队列元素个数
    int size()
    {
        std::unique_lock<std::mutex> lock(_mtx);
        return _list.size();
    }

    // 判断队列是否为空
    bool empty()
    {
        std::unique_lock<std::mutex> lock(_mtx);
        return _list.empty();
    }

    // 阻塞线程
    void wait()
    {
        std::unique_lock<std::mutex> lock(_mtx);

        // 这里传入的锁得是unique_lock的，因为将来线程wait的时候是要释放锁的，条件满足又要申请锁，所以lock_guard这种纯RAII风格的锁无法做到这一点
        // 那为什么不选用原生的mutex锁，而选择使用unique_lock呢？
        // 主要还是因为unique_lock是RAII的，使用起来要比原生的mutex更为灵活和安全！因为原生的锁使用不当会造成死锁问题
        _cond.wait(lock);
    }

private:
    // 线程的同步与互斥
    std::mutex _mtx;
    std::condition_variable _cond;

    // 有中间元素删除的需要，所以使用list
    std::list<T> _list;
};

// 接下来实现匹配队列的管理，跟之前一样，上面的只是先描述，接下来做的就是再组织
// 在匹配队列管理这里，我们要创建三个不同档次的匹配队列，对用户进行划分等级的对战匹配
// 同时还需要创建三个分别匹配不同档次阻塞队列的消费线程。创建房间肯定得需要room_manager的管理句柄，
// 将用户添加到不同档次的匹配队列里面，那就需要拿到该用户的score分数，那就还需要user_table的管理句柄，
// 在两个用户对战匹配成功后，我们需要给客户端，也就是前端浏览器，返回一个json响应，告知双方用户对战匹配成功，
// 所以还需要一个在线用户管理模块句柄也就是online_manager类的对象指针_om

class match_manager
{
public:
    match_manager(user_table *ut, online_manager *om, room_manager *rm)
        : _ut(ut), _om(om), _rm(rm), _th_bronze(&match_manager::th_bronze_entry, this), _th_silver(&match_manager::th_silver_entry, this), _th_golden(&match_manager::th_golden_entry, this)
    {
    }

    ~match_manager()
    {
    }

    // 当服务器收到客户端的对战匹配请求后，需要将用户添加到指定的匹配队列里面
    bool add(int uid)
    {
        // 添加用户到指定匹配队列
        // 需要先通过_ut句柄拿到玩家的天梯分数值，根据天梯分数的不同，调用不同的阻塞队列，进行玩家的push

        // 1. 判断uid是什么级别的，所以需要用到数据库查询
        Json::Value user;
        bool ret = _ut->select_by_userid(uid, user);
        if (ret == false)
        {
            DLOG("获取%d用户信息失败", uid);
            return false;
        }

        // 2. 添加到指定队列
        int score = user["score"].asInt();
        if (score < 2000)
            _q_bronze.push(uid);
        else if (score >= 2000 && score < 3000)
            _q_silver.push(uid);
        else
            _q_golden.push(uid);

        return true;
    }

    // 当服务器收到客户端的停止对战匹配请求后，需要将用户从特定的匹配队列中移除
    bool del(int uid)
    {
        // 移除玩家的逻辑也是同样如此，需要先根据天梯分数确定出玩家所在的匹配队列
        // 然后调用我们之前实现好的remove接口，进行指定玩家的移除
        // 1. 判断uid是什么级别的，所以需要用到数据库查询
        Json::Value user;
        bool ret = _ut->select_by_userid(uid, user);
        if (ret == false)
        {
            DLOG("获取%d用户信息失败", uid);
            return false;
        }

        // 2. 从指定队列删除该用户
        int score = user["score"].asInt();
        if (score < 2000)
            _q_bronze.remove(uid);
        else if (score >= 2000 && score < 3000)
            _q_silver.remove(uid);
        else
            _q_golden.remove(uid);

        return true;
    }

private:
    // 由于我们创建出来了三个线程，每个线程肯定要有对应的线程执行入口函数，
    // 所以在私有方法里面分别实现了三个线程的入口函数，由于每个线程执行的逻辑都是一样的，
    // 所以又实现了一个统一的私有方法handle_match
    void handler_match(match_queue<int> &mq)
    {
        // 线程一般创建出来之后是要被join的，但在这里其实不用，一旦线程跑起来，就是死循环的，不断检测队列中是否有两个用户
        while (true) // 死循环，不断判断是否有两个用户
        {
            // 1. 判断队列是否有两个元素
            if (mq.size() >= 2)
            {
                int uid1, uid2;
                bool ret1 = mq.pop(uid1);
                if (ret1 == false)
                    continue; // 走到这，说明有两个用户，但是第一个用户取失败了，则继续

                bool ret2 = mq.pop(uid2);
                if (ret2 == false)
                {
                    // 走到这，说明有两个用户，第一个用户取成功了，但是第二个用户取失败了
                    // 此时要重新要用户1放回去，让他重新匹配，再continue
                    mq.push(uid1);
                    continue;
                }

                // 走到这，说明两个用户都取出来了
                // 下一步就是判断两个用户是否都在游戏大厅中
                // 只要有一个不在游戏大厅中，那就说明此时有某个人掉线了，可能断网了，或者他自己关闭游戏大厅的页面了
                // 那么此时我们要把相对的那个人重新放回匹配队列中，然后continue重新执行循环逻辑

                // 2. 判断两人是否在线
                wsserver_t::connection_ptr conn1 = _om->get_conn_from_hall(uid1);
                if (conn1.get() == nullptr)
                {
                    // 用户1不在线，把用户2重新放到阻塞队列
                    mq.push(uid2);
                    continue;
                }
                wsserver_t::connection_ptr conn2 = _om->get_conn_from_hall(uid2);
                if (conn2.get() == nullptr)
                {
                    // 用户2不在线，把用户1重新放到阻塞队列
                    mq.push(uid1);
                    continue;
                }

                // 3. 走到这，就可以为他们创建房间了
                room_ptr rp = _rm->create_room(uid1, uid2);
                if (rp.get() == nullptr)
                {
                    // 房间创建失败，把这两个人重新放回队列，等待下一次匹配
                    mq.push(uid1);
                    mq.push(uid2);
                    continue;
                }

                // 4. 走到这，房间也创建成功了，所以对两个用户进行匹配成功消息的相应
                Json::Value resp;
                resp["optype"] = "match_success";
                resp["result"] = true;

                std::string body;
                json_util::serialize(resp, body); // 序列化

                conn1->send(body);
                conn2->send(body);
                DLOG("match_success_body: %s", body.c_str());
            }
            else
            {
                // 房间中用户不足两人
                mq.wait();
            }
        }
    }

    void th_bronze_entry() { handler_match(_q_bronze); }
    void th_silver_entry() { handler_match(_q_silver); }
    void th_golden_entry() { handler_match(_q_golden); }

private:
    // 三个不同等级的匹配队列
    match_queue<int> _q_bronze;
    match_queue<int> _q_silver;
    match_queue<int> _q_golden;

    // 三个匹配线程对应每个队列
    std::thread _th_bronze;
    std::thread _th_silver;
    std::thread _th_golden;

    // 三个管理模块
    user_table *_ut;
    online_manager *_om;
    room_manager *_rm;
};

#endif